package prom

import (
	"context"
	"fmt"
	"testing"
	"time"

	"github.com/vladimirvivien/ktop/prom"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/rest"
)

// mockPodObject implements metav1.Object for testing
type mockPodObject struct {
	namespace string
	name      string
}

func (m *mockPodObject) GetNamespace() string                                       { return m.namespace }
func (m *mockPodObject) SetNamespace(namespace string)                              { m.namespace = namespace }
func (m *mockPodObject) GetName() string                                            { return m.name }
func (m *mockPodObject) SetName(name string)                                        { m.name = name }
func (m *mockPodObject) GetGenerateName() string                                    { return "" }
func (m *mockPodObject) SetGenerateName(name string)                                {}
func (m *mockPodObject) GetUID() types.UID                                          { return "" }
func (m *mockPodObject) SetUID(uid types.UID)                                       {}
func (m *mockPodObject) GetResourceVersion() string                                 { return "" }
func (m *mockPodObject) SetResourceVersion(version string)                          {}
func (m *mockPodObject) GetGeneration() int64                                       { return 0 }
func (m *mockPodObject) SetGeneration(generation int64)                             {}
func (m *mockPodObject) GetSelfLink() string                                        { return "" }
func (m *mockPodObject) SetSelfLink(selfLink string)                                {}
func (m *mockPodObject) GetCreationTimestamp() metav1.Time                          { return metav1.Time{} }
func (m *mockPodObject) SetCreationTimestamp(timestamp metav1.Time)                 {}
func (m *mockPodObject) GetDeletionTimestamp() *metav1.Time                         { return nil }
func (m *mockPodObject) SetDeletionTimestamp(timestamp *metav1.Time)                {}
func (m *mockPodObject) GetDeletionGracePeriodSeconds() *int64                      { return nil }
func (m *mockPodObject) SetDeletionGracePeriodSeconds(*int64)                       {}
func (m *mockPodObject) GetLabels() map[string]string                               { return nil }
func (m *mockPodObject) SetLabels(labels map[string]string)                         {}
func (m *mockPodObject) GetAnnotations() map[string]string                          { return nil }
func (m *mockPodObject) SetAnnotations(annotations map[string]string)               {}
func (m *mockPodObject) GetFinalizers() []string                                    { return nil }
func (m *mockPodObject) SetFinalizers(finalizers []string)                          {}
func (m *mockPodObject) GetOwnerReferences() []metav1.OwnerReference                { return nil }
func (m *mockPodObject) SetOwnerReferences([]metav1.OwnerReference)                 {}
func (m *mockPodObject) GetManagedFields() []metav1.ManagedFieldsEntry              { return nil }
func (m *mockPodObject) SetManagedFields(managedFields []metav1.ManagedFieldsEntry) {}

// MockMetricsStore implements prom.MetricsStore for testing
type MockMetricsStore struct {
	metrics map[string]map[string]float64 // metricName -> labels -> value
}

func NewMockMetricsStore() *MockMetricsStore {
	return &MockMetricsStore{
		metrics: make(map[string]map[string]float64),
	}
}

func (m *MockMetricsStore) AddMetrics(metrics *prom.ScrapedMetrics) error {
	return nil
}

func (m *MockMetricsStore) QueryLatest(metricName string, labelMatchers map[string]string) (float64, error) {
	if metricValues, ok := m.metrics[metricName]; ok {
		// For simplicity, return first matching value
		// In real implementation would match labels properly
		for _, value := range metricValues {
			return value, nil
		}
	}
	return 0, nil
}

func (m *MockMetricsStore) QueryLatestSum(metricName string, labelMatchers map[string]string) (float64, error) {
	if metricValues, ok := m.metrics[metricName]; ok {
		// Sum all matching values (simulates multiple containers)
		var total float64
		for _, value := range metricValues {
			total += value
		}
		return total, nil
	}
	return 0, nil
}

func (m *MockMetricsStore) QueryRange(metricName string, labelMatchers map[string]string, start, end time.Time) ([]*prom.MetricSample, error) {
	if metricValues, ok := m.metrics[metricName]; ok {
		// Generate two samples 40 seconds apart for rate testing
		now := time.Now()
		firstValue := 0.0
		lastValue := 0.0

		// Get the metric value (simplified - just use first value found)
		for _, value := range metricValues {
			lastValue = value
			break
		}

		// First sample: 40 seconds ago with base value
		// Last sample: now with value increased by 4.0 (simulates 4 CPU seconds over 40s = 0.1 cores = 100m)
		firstValue = lastValue - 4.0
		if firstValue < 0 {
			firstValue = 0
		}

		samples := []*prom.MetricSample{
			{Timestamp: now.Add(-40 * time.Second).UnixMilli(), Value: firstValue},
			{Timestamp: now.UnixMilli(), Value: lastValue},
		}
		return samples, nil
	}
	return nil, fmt.Errorf("metric %s not found", metricName)
}

func (m *MockMetricsStore) QueryRangePerSeries(metricName string, labelMatchers map[string]string, start, end time.Time) (map[string][]*prom.MetricSample, error) {
	if metricValues, ok := m.metrics[metricName]; ok {
		// Generate two samples 40 seconds apart for rate testing
		now := time.Now()

		result := make(map[string][]*prom.MetricSample)

		// Create a series for each label set stored
		for labels, lastValue := range metricValues {
			// First sample: 40 seconds ago with base value
			// Last sample: now with value increased by 4.0 (simulates 4 CPU seconds over 40s = 0.1 cores = 100m)
			firstValue := lastValue - 4.0
			if firstValue < 0 {
				firstValue = 0
			}

			samples := []*prom.MetricSample{
				{Timestamp: now.Add(-40 * time.Second).UnixMilli(), Value: firstValue},
				{Timestamp: now.UnixMilli(), Value: lastValue},
			}
			result[labels] = samples
		}
		return result, nil
	}
	return nil, fmt.Errorf("metric %s not found", metricName)
}

func (m *MockMetricsStore) GetMetricNames() []string {
	names := make([]string, 0, len(m.metrics))
	for name := range m.metrics {
		names = append(names, name)
	}
	return names
}

func (m *MockMetricsStore) GetLabelValues(labelName string) []string {
	return []string{}
}

func (m *MockMetricsStore) Cleanup() error {
	return nil
}

// SetMetric is a helper for tests to set metric values
func (m *MockMetricsStore) SetMetric(metricName, labels string, value float64) {
	if m.metrics[metricName] == nil {
		m.metrics[metricName] = make(map[string]float64)
	}
	m.metrics[metricName][labels] = value
}

func TestNewPromMetricsSource(t *testing.T) {
	config := DefaultPromConfig()
	source, err := NewPromMetricsSource(&rest.Config{}, config)

	if err != nil {
		t.Fatalf("NewPromMetricsSource failed: %v", err)
	}

	if source == nil {
		t.Fatal("Expected non-nil source")
	}

	if source.controller == nil {
		t.Error("Expected controller to be initialized")
	}

	if source.config == nil {
		t.Error("Expected config to be set")
	}

	if source.healthy {
		t.Error("Expected source to be initially unhealthy")
	}
}

func TestNewPromMetricsSource_WithNilConfig(t *testing.T) {
	source, err := NewPromMetricsSource(&rest.Config{}, nil)

	if err != nil {
		t.Fatalf("NewPromMetricsSource with nil config failed: %v", err)
	}

	if source.config == nil {
		t.Error("Expected default config to be used")
	}

	// Verify default config values
	if source.config.ScrapeInterval != 5*time.Second {
		t.Errorf("Expected default scrape interval 5s, got %v", source.config.ScrapeInterval)
	}

	if source.config.RetentionTime != 1*time.Hour {
		t.Errorf("Expected default retention time 1h, got %v", source.config.RetentionTime)
	}

	if source.config.MaxSamples != 10000 {
		t.Errorf("Expected default max samples 10000, got %d", source.config.MaxSamples)
	}
}

func TestDefaultPromConfig(t *testing.T) {
	config := DefaultPromConfig()

	if !config.Enabled {
		t.Error("Expected Enabled to be true")
	}

	if config.ScrapeInterval != 5*time.Second {
		t.Errorf("Expected ScrapeInterval 5s, got %v", config.ScrapeInterval)
	}

	if config.RetentionTime != 1*time.Hour {
		t.Errorf("Expected RetentionTime 1h, got %v", config.RetentionTime)
	}

	if config.MaxSamples != 10000 {
		t.Errorf("Expected MaxSamples 10000, got %d", config.MaxSamples)
	}

	// Check default components
	expectedComponents := []prom.ComponentType{
		prom.ComponentKubelet,
		prom.ComponentCAdvisor,
		prom.ComponentAPIServer,
	}

	if len(config.Components) != len(expectedComponents) {
		t.Errorf("Expected %d components, got %d", len(expectedComponents), len(config.Components))
	}

	for i, expected := range expectedComponents {
		if config.Components[i] != expected {
			t.Errorf("Expected component %v at index %d, got %v", expected, i, config.Components[i])
		}
	}
}

func TestGetAvailableMetrics(t *testing.T) {
	source, _ := NewPromMetricsSource(&rest.Config{}, nil)

	metrics := source.GetAvailableMetrics()

	expectedMetrics := []string{
		"cpu",
		"memory",
		"network_rx",
		"network_tx",
		"load_1m",
		"load_5m",
		"load_15m",
		"pod_count",
		"container_count",
		"disk_usage",
	}

	if len(metrics) != len(expectedMetrics) {
		t.Errorf("Expected %d metrics, got %d", len(expectedMetrics), len(metrics))
	}

	// Check all expected metrics are present
	metricMap := make(map[string]bool)
	for _, m := range metrics {
		metricMap[m] = true
	}

	for _, expected := range expectedMetrics {
		if !metricMap[expected] {
			t.Errorf("Expected metric %s not found", expected)
		}
	}
}

func TestIsHealthy_InitiallyUnhealthy(t *testing.T) {
	source, _ := NewPromMetricsSource(&rest.Config{}, nil)

	if source.IsHealthy() {
		t.Error("Expected source to be initially unhealthy")
	}
}

func TestGetSourceInfo(t *testing.T) {
	source, _ := NewPromMetricsSource(&rest.Config{}, nil)

	info := source.GetSourceInfo()

	if info.Type != "prometheus" {
		t.Errorf("Expected Type 'prometheus', got '%s'", info.Type)
	}

	if info.Version != "v1.0.0" {
		t.Errorf("Expected Version 'v1.0.0', got '%s'", info.Version)
	}

	if info.Healthy {
		t.Error("Expected Healthy to be false initially")
	}

	if info.ErrorCount != 0 {
		t.Errorf("Expected ErrorCount 0, got %d", info.ErrorCount)
	}

	if info.MetricsCount != 0 {
		t.Errorf("Expected MetricsCount 0 (no store yet), got %d", info.MetricsCount)
	}
}

func TestHandleError(t *testing.T) {
	source, _ := NewPromMetricsSource(&rest.Config{}, nil)

	// Initially no errors
	if source.errorCount != 0 {
		t.Errorf("Expected initial errorCount 0, got %d", source.errorCount)
	}

	// Simulate error callback
	testErr := context.Canceled
	source.handleError(prom.ComponentKubelet, testErr)

	// Check error was recorded
	if source.errorCount != 1 {
		t.Errorf("Expected errorCount 1, got %d", source.errorCount)
	}

	if source.lastError != testErr {
		t.Errorf("Expected lastError to be set")
	}

	if source.healthy {
		t.Error("Expected healthy to be false after error")
	}
}

func TestHandleMetricsCollected(t *testing.T) {
	source, _ := NewPromMetricsSource(&rest.Config{}, nil)

	// Initially unhealthy
	source.healthy = false

	// Simulate metrics collected callback
	source.handleMetricsCollected(prom.ComponentKubelet, &prom.ScrapedMetrics{})

	// Check health was updated
	if !source.healthy {
		t.Error("Expected healthy to be true after successful collection")
	}

	if source.lastError != nil {
		t.Error("Expected lastError to be nil after successful collection")
	}

	if source.lastScrape.IsZero() {
		t.Error("Expected lastScrape to be set")
	}
}

func TestRecordError(t *testing.T) {
	source, _ := NewPromMetricsSource(&rest.Config{}, nil)

	testErr := context.DeadlineExceeded
	source.recordError(testErr)

	if source.errorCount != 1 {
		t.Errorf("Expected errorCount 1, got %d", source.errorCount)
	}

	if source.lastError != testErr {
		t.Error("Expected lastError to be set to testErr")
	}

	if source.healthy {
		t.Error("Expected healthy to be false")
	}

	// Record another error
	source.recordError(testErr)

	if source.errorCount != 2 {
		t.Errorf("Expected errorCount 2, got %d", source.errorCount)
	}
}

func TestGetNodeMetrics_UnhealthySource(t *testing.T) {
	source, _ := NewPromMetricsSource(&rest.Config{}, nil)

	// Ensure source is unhealthy
	source.healthy = false

	_, err := source.GetNodeMetrics(context.Background(), "test-node")

	if err == nil {
		t.Error("Expected error when source is unhealthy")
	}

	if err.Error() != "prometheus source is not healthy" {
		t.Errorf("Expected 'prometheus source is not healthy' error, got '%v'", err)
	}
}

func TestGetNodeMetrics_NoStore(t *testing.T) {
	source, _ := NewPromMetricsSource(&rest.Config{}, nil)

	// Set healthy but no store
	source.healthy = true
	source.store = nil

	_, err := source.GetNodeMetrics(context.Background(), "test-node")

	if err == nil {
		t.Error("Expected error when store is nil")
	}

	if err.Error() != "metrics store not initialized" {
		t.Errorf("Expected 'metrics store not initialized' error, got '%v'", err)
	}
}

func TestGetPodMetrics_UnhealthySource(t *testing.T) {
	source, _ := NewPromMetricsSource(&rest.Config{}, nil)

	source.healthy = false

	_, err := source.GetPodMetrics(context.Background(), "default", "test-pod")

	if err == nil {
		t.Error("Expected error when source is unhealthy")
	}
}

func TestGetPodMetrics_NoStore(t *testing.T) {
	source, _ := NewPromMetricsSource(&rest.Config{}, nil)

	source.healthy = true
	source.store = nil

	_, err := source.GetPodMetrics(context.Background(), "default", "test-pod")

	if err == nil {
		t.Error("Expected error when store is nil")
	}
}

func TestGetAllPodMetrics_UnhealthySource(t *testing.T) {
	source, _ := NewPromMetricsSource(&rest.Config{}, nil)

	source.healthy = false

	_, err := source.GetAllPodMetrics(context.Background())

	if err == nil {
		t.Error("Expected error when source is unhealthy")
	}
}

func TestStop(t *testing.T) {
	source, _ := NewPromMetricsSource(&rest.Config{}, nil)

	// Stop should not error even if not started
	err := source.Stop()

	// This may error since controller was never started
	// but we're just testing that Stop() can be called
	_ = err
}

func TestGetNodeMetrics_WithMockStore(t *testing.T) {
	source, _ := NewPromMetricsSource(&rest.Config{}, nil)

	// Set up mock store with test data
	// Using correct cAdvisor metric names with id="/" for node-level
	mockStore := NewMockMetricsStore()
	// CPU: Set to 104.0 (counter value), QueryRange will return samples that calculate to 0.1 cores = 100m
	mockStore.SetMetric("container_cpu_usage_seconds_total", "id:/", 104.0)
	mockStore.SetMetric("container_memory_working_set_bytes", "id:/", 1024*1024*1024) // 1GB
	// Network metrics: cAdvisor counters, rate = 4.0/40s = 0.1 bytes/sec
	mockStore.SetMetric("container_network_receive_bytes_total", "id:/", 1024.0)
	mockStore.SetMetric("container_network_transmit_bytes_total", "id:/", 512.0)
	// Disk I/O metrics: cAdvisor counters, rate = 4.0/40s = 0.1 bytes/sec
	mockStore.SetMetric("container_fs_reads_bytes_total", "id:/", 2048.0)
	mockStore.SetMetric("container_fs_writes_bytes_total", "id:/", 1024.0)
	// Note: Load averages (node_load1/5/15) are NOT available from kubelet/cAdvisor
	// They require node_exporter, so we don't set them here and expect 0 values
	mockStore.SetMetric("kubelet_running_pods", "node:test-node", 15)
	mockStore.SetMetric("container_count", "node:test-node", 42)

	source.store = mockStore
	source.healthy = true

	metrics, err := source.GetNodeMetrics(context.Background(), "test-node")

	if err != nil {
		t.Fatalf("GetNodeMetrics failed: %v", err)
	}

	if metrics == nil {
		t.Fatal("Expected non-nil metrics")
	}

	if metrics.NodeName != "test-node" {
		t.Errorf("Expected NodeName 'test-node', got '%s'", metrics.NodeName)
	}

	// Verify CPU usage (rate calculation: 4 CPU seconds over 40s = 0.1 cores = 100 millicores)
	if metrics.CPUUsage == nil {
		t.Error("Expected CPUUsage to be set")
	} else if metrics.CPUUsage.MilliValue() != 100 {
		t.Errorf("Expected CPU 100m (from rate calculation), got %d", metrics.CPUUsage.MilliValue())
	}

	// Verify Memory usage (1GB)
	if metrics.MemoryUsage == nil {
		t.Error("Expected MemoryUsage to be set")
	} else if metrics.MemoryUsage.Value() != 1024*1024*1024 {
		t.Errorf("Expected Memory 1GB, got %d", metrics.MemoryUsage.Value())
	}

	// Verify Network RX rate (calculated from counter: 4.0 delta / 40s = 0.1 bytes/sec)
	if metrics.NetworkRxRate != 0.1 {
		t.Errorf("Expected NetworkRxRate 0.1 bytes/sec, got %f", metrics.NetworkRxRate)
	}

	// Verify Network TX rate (calculated from counter: 4.0 delta / 40s = 0.1 bytes/sec)
	if metrics.NetworkTxRate != 0.1 {
		t.Errorf("Expected NetworkTxRate 0.1 bytes/sec, got %f", metrics.NetworkTxRate)
	}

	// Verify Disk Read rate (calculated from counter: 4.0 delta / 40s = 0.1 bytes/sec)
	if metrics.DiskReadRate != 0.1 {
		t.Errorf("Expected DiskReadRate 0.1 bytes/sec, got %f", metrics.DiskReadRate)
	}

	// Verify Disk Write rate (calculated from counter: 4.0 delta / 40s = 0.1 bytes/sec)
	if metrics.DiskWriteRate != 0.1 {
		t.Errorf("Expected DiskWriteRate 0.1 bytes/sec, got %f", metrics.DiskWriteRate)
	}

	// Verify Load averages are 0 (not available from kubelet/cAdvisor without node_exporter)
	if metrics.LoadAverage1m != 0 {
		t.Errorf("Expected LoadAverage1m 0 (not available), got %f", metrics.LoadAverage1m)
	}
	if metrics.LoadAverage5m != 0 {
		t.Errorf("Expected LoadAverage5m 0 (not available), got %f", metrics.LoadAverage5m)
	}
	if metrics.LoadAverage15m != 0 {
		t.Errorf("Expected LoadAverage15m 0 (not available), got %f", metrics.LoadAverage15m)
	}

	// Verify Pod count
	if metrics.PodCount != 15 {
		t.Errorf("Expected PodCount 15, got %d", metrics.PodCount)
	}

	// Verify Container count
	if metrics.ContainerCount != 42 {
		t.Errorf("Expected ContainerCount 42, got %d", metrics.ContainerCount)
	}
}

func TestGetPodMetrics_WithMockStore(t *testing.T) {
	source, _ := NewPromMetricsSource(&rest.Config{}, nil)

	mockStore := NewMockMetricsStore()
	// CPU: Set to 104.0 (counter value), QueryRange will return samples that calculate to 0.1 cores = 100m
	mockStore.SetMetric("container_cpu_usage_seconds_total", "pod:test-pod", 104.0)
	mockStore.SetMetric("container_memory_working_set_bytes", "pod:test-pod", 256*1024*1024) // 256MB

	source.store = mockStore
	source.healthy = true

	metrics, err := source.GetPodMetrics(context.Background(), "default", "test-pod")

	if err != nil {
		t.Fatalf("GetPodMetrics failed: %v", err)
	}

	if metrics == nil {
		t.Fatal("Expected non-nil metrics")
	}

	if metrics.PodName != "test-pod" {
		t.Errorf("Expected PodName 'test-pod', got '%s'", metrics.PodName)
	}

	if metrics.Namespace != "default" {
		t.Errorf("Expected Namespace 'default', got '%s'", metrics.Namespace)
	}

	// Should have at least one container
	if len(metrics.Containers) == 0 {
		t.Error("Expected at least one container")
	} else {
		container := metrics.Containers[0]

		if container.CPUUsage == nil {
			t.Error("Expected container CPUUsage to be set")
		} else if container.CPUUsage.MilliValue() != 100 {
			t.Errorf("Expected container CPU 100m (from rate calculation), got %d", container.CPUUsage.MilliValue())
		}

		if container.MemoryUsage == nil {
			t.Error("Expected container MemoryUsage to be set")
		} else if container.MemoryUsage.Value() != 256*1024*1024 {
			t.Errorf("Expected container Memory 256MB, got %d", container.MemoryUsage.Value())
		}
	}
}

func TestGetAllPodMetrics_WithMockStore(t *testing.T) {
	source, _ := NewPromMetricsSource(&rest.Config{}, nil)

	mockStore := NewMockMetricsStore()
	// Note: This test is simplified - GetAllPodMetrics implementation needs work
	// but we're testing that it doesn't crash with a mock store

	source.store = mockStore
	source.healthy = true

	metrics, err := source.GetAllPodMetrics(context.Background())

	if err != nil {
		t.Fatalf("GetAllPodMetrics failed: %v", err)
	}

	// With empty mock store, should return empty slice (not nil)
	if metrics == nil {
		// This is acceptable for now - GetAllPodMetrics returns nil when no pods found
		// In future PR, we should ensure it returns empty slice instead
		t.Log("Note: GetAllPodMetrics returns nil for empty store - consider returning empty slice")
	}
}

func TestGetSourceInfo_WithStore(t *testing.T) {
	source, _ := NewPromMetricsSource(&rest.Config{}, nil)

	mockStore := NewMockMetricsStore()
	mockStore.SetMetric("test_metric_1", "label", 1.0)
	mockStore.SetMetric("test_metric_2", "label", 2.0)

	source.store = mockStore
	source.healthy = true
	source.errorCount = 3
	source.lastScrape = time.Date(2025, 10, 26, 12, 0, 0, 0, time.UTC)

	info := source.GetSourceInfo()

	if info.MetricsCount != 2 {
		t.Errorf("Expected MetricsCount 2, got %d", info.MetricsCount)
	}

	if info.ErrorCount != 3 {
		t.Errorf("Expected ErrorCount 3, got %d", info.ErrorCount)
	}

	if !info.Healthy {
		t.Error("Expected Healthy to be true")
	}

	if !info.LastScrape.Equal(source.lastScrape) {
		t.Errorf("Expected LastScrape to match source.lastScrape")
	}
}

func TestGetMetricsForPod_NotImplemented(t *testing.T) {
	source, _ := NewPromMetricsSource(&rest.Config{}, nil)

	source.store = NewMockMetricsStore()
	source.healthy = true

	// Create a mock pod object
	mockPod := &mockPodObject{
		namespace: "test-namespace",
		name:      "test-pod",
	}

	metrics, err := source.GetMetricsForPod(context.Background(), mockPod)

	if err != nil {
		t.Errorf("Expected no error, got %v", err)
	}

	if metrics == nil {
		t.Error("Expected metrics to be returned")
	}

	if metrics.Namespace != "test-namespace" {
		t.Errorf("Expected namespace 'test-namespace', got '%s'", metrics.Namespace)
	}

	if metrics.PodName != "test-pod" {
		t.Errorf("Expected pod name 'test-pod', got '%s'", metrics.PodName)
	}
}
